Demos
Introducing Dissolve -
The effect of taking one image and slowly replacing it with another image, pixel by pixel in a random order, is often called a dissolve. I began working on a dissolve effect of my own using Liberty Basic as a result of a question which was asked on the Liberty Basic Conforums forum.
It is easy to build a window with a graphicbox and display an image in Liberty Basic. Equally easy I determined was placing two graphicboxes side by side and then placing two unique images into each. From there it was a relatively easy mater to examine a pixel in one of the graphicboxes and then set the corresponding pixel in the other graphicbox to the same color.
The challenge I immediately recognized (upon try it) was duplicating this process without displaying the second graphicbox. I tried placing the graphicbox off-screen and loading an image into it and then using the process described above to examine pixels in the hidden graphicbox and set them in the visible graphicbox. The result was overwriting the destination graphicbox with black pixels. The hidden graphicbox had no image to examine.
The program relies on many API calls, and I knew the answer lay in an even deeper penetration of the Windows GDI calls. I was using GetPixel and SetPixel to manipulate the pixel level graphics. These calls require knowledge of the device-context that each of the two graphicboxes belong to. It seemed what I needed was an off-screen device-context that I could load the image into behind the scenes and then do my pixel manipulation from there.
I played around with a couple options, but could not get it right. Finally Alyce Watson supplied the key that unlocked the mystery for me. It turned out to be Compatible Device-Context and a series of API calls for working with it. She kindly shared a demo from which I was able to adapt the API calls for my needs.
Here in this article I am going to explain how that final program works, and supply you with the source code and images for the project. These are all part of the fade.zip archive which is part of this months newsletter archive. Don't forget you must download the newsletter to get the attached files in the archive.
Looking Under The Hood -
The main portion of the program where the work is performed is fairly short, but the is some preparation required before the dissolve (originally called a fade) can be performed. The two main things that must be done are the loading of the images into memory and the dynamic building of the dissolve array.
I chose to build an array of all valid pixel location pairs (x and y coordinates) and then scramble these randomly, rather than later relying on randomly picking points and simply running until the whole destination image was replaced by the source image. The problem with the later approach (which is the brute force method) is that you really never know whether you have replaced every pixel, so you must run nearly indefinably, which is not very efficient. By building a random array of valid pixels, we know exactly how many passes are required through the loop to move a source pixel to the destination image to completely replace the image. This affords the greatest efficiency. The caveat is a longer upfront initialization cycle.
I chose to open a window and display a warning message to the user as the program initialized. Here is the code that starts out the program:
'fade3.bas - by Brad Moore, copyright 2004
'with gracious thanks to Alyce Watson for many of the API calls
' and help with the Windows GDI calls.
'This program is open source. If you fork it, please rename your
' creation. - Thanks
'-------------------------------------------------------------------------
'I am going to prep the project by building a fade array of points to fade
'this will make the actual fade run faster
WindowWidth = 390 : WindowHeight = 85
UpperLeftX = INT((DisplayWidth-WindowWidth)/2)
UpperLeftY = INT((DisplayHeight-WindowHeight)/2)
statictext #init.st1, "Preparing Data - please wait...", 15, 10, 380, 35
Open "Intializing" for Window as #init
#init "trapclose [cantquit]"
#init "font arial 18 bold"
The code above opens a window and simply displays a message to the user advising them to wait.
Next the images are loaded:
'load the Original BMP
loadbmp "leaves", "leaves.bmp"
'load the Source BMP
loadbmp "coins", "coins.bmp"
'get the handle of the source BMP
hPic=hBmp("coins")
I used the LB statement loadbmp which places the bitmaps into memory. The Coins bmp is the source image that will be laid over the Leaves bmp. I will put this into a Compatible DC after the main window is opened. To prepare for this I need to get the handle of the bitmap in memory. This is a windows resource handle. The LB function hBmp returns this value for us. It is stored in hPic for later use.
Next I prepare some foundation data, including a couple arrays to hold the x and y coordinates I referred to earlier:
'drawing size - make sure this is divisible by 2
maxx = 180
maxy = 180
'build an array of x and y pixels to swap
'I will move groups of four pixels so the fade works faster.
xbound = (maxx/2)
ybound = (maxy/2)
maxpixels = xbound * ybound
dim pixelsetx(maxpixels)
dim pixelsety(maxpixels)
I created my images to be exactly the same size, so that there was no hassle with size translations. I could have obtained the bmp sizes with a couple API calls, but I already knew the sizes, so I simply hard coded these. Maxx and maxy hold these values.
I plan to work in pixel sets of two across and two down (four pixels at a time) to speed along the dissolve process. For this reason I created the xbound and ybound variables which hold the values of maxx and maxy divided by two.
The total maximum pixels are the product of xbound and ybound. This is stored in maxpixels. This value is used in several For-Next loops and is the grand total iterations required to completely replace one image with the other. The final element is the creation of a pair of arrays that will hold the corresponding x and y coordinates of each pixel group that will be swapped. Since I am working with both an 'x' and a 'y' set of coordinates, we require a separate array for each value. We will carefully keep the pairs together though.
The next step is to populate the arrays. This is done with the following set of nested loops:
for x = 1 to xbound
for y = 1 to ybound
pixelsetx(((x-1)*xbound)+y) = ((x-1)*2)
pixelsety(((x-1)*xbound)+y) = ((y-1)*2)
next y
next x
We will be filling these with xbound * ybound elements. The index value for the arrays is built with a fairly complex formula that insures there is a value populated for every array element. The values stored are derived from the loop operation and are spaced every other pixel in both the 'x' and 'y' directions beginning with zero. If the explanation seems confusing, I recommend running the program in debug mode and watching the values as they are populated.
The real trick to the randomness of the dissolve operation is in the way the sets of ordered pixels (which were populated in the code we just looked at) are randomized in the code below:
'now a few iterations to mix stuff up
for q = 1 to 15000
src = int(rnd(0)*(maxpixels))+1
dest = src
while dest = src
dest = int(rnd(0)*(maxpixels))+1
wend
'now swap the elements
tempx = pixelsetx(dest)
tempy = pixelsety(dest)
pixelsetx(dest) = pixelsetx(src)
pixelsety(dest) = pixelsety(src)
pixelsetx(src) = tempx
pixelsety(src) = tempy
next q
What this code does is simply choose a random source array element location (in: src = int(rnd(0)*(maxpixels))+1) and a random destination array element location (in: dest = int(rnd(0)*(maxpixels))+1) and swap them 15000 times.
There is a little trick in the code that insures the destination is not the same as the source. Before choosing the destination element I set it equal to the source element. Then a WHILE loop is entered. The code will only exit the loop when the two are no longer equal.
The swap must be performed in three steps for each array. The destination value is saved in a temporary variable, then the source is written over the destination, and finally the temporary variable (holding the destination) is written over the source. This three step process is required because LB does not have a native SWAP function. I could have written my own SWAP function, but that would have cost valuable time in very time consuming process.
Finally we do a little bit of clean up and we are done:
[cantquit]
scan
close #init
reset = 0 'tells program whether to reset image first before dissolve
The first two lines simply flush any mouse clicks on the [X] close button if there were any. In the earlier code they were routed to the label [cantquit] which basically causes them to be ignored if they were present.
Next close the window and set a special tacking flag I use later to decide which button is shown on the main form.
Next is the preparing the program's main window GUI and opening the window. This is basic LB fair:
[WindowSetup]
NOMAINWIN
WindowWidth = 308 : WindowHeight = 240
UpperLeftX = INT((DisplayWidth-WindowWidth)/2)
UpperLeftY = INT((DisplayHeight-WindowHeight)/2)
[ControlSetup]
graphicbox #main.gb1, 10, 10, 180, 180
button #main.doit, "Do It!",[doit],UL, 195, 150, 95, 40
Open "Window Title" for Window as #main
#main "trapclose [quit]"
#main "font ms_sans_serif 10 bold"
Follow this with code to fill the graphicbox with a white background and then display the 'leaves' bitmap:
#main.gb1 "down; fill White; flush"
#main.gb1 "drawbmp leaves 0 0"
#main.gb1 "flush"
The magic of the program begins in the next sections of code. This is where we get the DC for the graphicbox and then create a compatible DC. Alyce has spent a fair amount of time in various graphics demos and articles explaining the inner workings of the windows GDI and specifically the Device Context. The Device Context is a unique container object that encapsulates the attributes present in any graphics drawing region in Windows. It is required to do any form of graphics in Windows. Liberty Basic graciously handles these for us behind the scenes when we are using native LB Drawing commands, but since we are expanding beyond the boundaries of Liberty Basic, we must take on some of the management of these objects.
We require the DC of the graphicbox for the later bit manipulations we are going to do. Originally I wrote this program using the setPixel dll call (which requires a DC), but I have since started using the BITBLT since it is faster moving multiple pixels at once. It too requires the DC of the graphicbox.
We will also be loading the source image "coins" into an off screen graphics area for copying from. This will be done by creating a Compatible DC that is similar to the DC used by the graphicbox. Lets see how this is done:
'get the window handle
hWnd = hwnd(#main)
'get handle to the graphicbox
hgb1 = hwnd(#main.gb1)
'get each DC for the graphicbox
gb1DC = GetDC(hgb1)
These three sections of code setup for and then retrieve the graphicbox DC. The DC is obtained from the function (courtesy of Alyce Watson) called GetDC which is shown here:
Function GetDC(hWnd)
CallDLL #user32, "GetDC",hWnd As Long,GetDC As Long
End Function
The next piece required is a DC that is compatible with the graphicbox DC. Since this DC does not yet exist, we will use the CreateCompatibleDC dll to create it:
'create a compatible DC (in memory)
chDC = CreateCompatibleDC(gb1DC)
The call is encapsulated in a function (also courtesy of Alyce) which uses the graphicbox DC as a pattern to create the compatibleDC. The function returns the handle of the compatibleDC. This is all accomplished in the following function:
Function CreateCompatibleDC(hDC)
CallDLL #gdi32,"CreateCompatibleDC", hDC As Long, CreateCompatibleDC As Long
End Function
The last part of getting ready is to put the "coins" bitmap into the compatibleDC. This is done by actually selecting the image from memory into the container. The selectObject dll (which is implemented in the selectobject function - thanks Alyce) is used to accomplish this. Here is how:
'select the Object (the picture in memory) into the compatible DC
result = SelectObject(chDC, hPic)
Remember we got the handle of the bitmap in memory when we loaded it earlier. We pass that handle along with the handle to the compatibleDC onto the selectObject dll. The function returns a result that can be used to check the status of the call. For this demo I did not bother to check it, but if you are developing for public release, you should always validate any call or function that has a return value.
The actual dll call is implemented in the following function:
Function SelectObject(hDC,hObject)
CallDLL #gdi32,"SelectObject",hDC As Long,hObject As Long,SelectObject As Long
'returns previously selected object
End Function
At this point we are prepared to turn the program over to the user and wait for interaction. All we need is a simple WAIT statement:
[loop]
Wait
There are only two branches that act as event handlers: "quit" and "do-it". The "quit" branch is used to clean up the program as it shuts down. It unloads the images, releases the DC from the graphicbox and deletes the DC we created and finally closes the window. It is important to remember to clean up, as leaving DCs and images loaded after the program ends will cause physical memory to be used in a manner that can not be reclaimed until the system is restarted. Here is the code:
[quit]
call ReleaseDC hWnd, gb1DC
call DeleteDC chDC
unloadbmp "leaves"
unloadbmp "coins"
close #main
END
The code calls two sub programs, ReleaseDC and DeleteDC. They are simple encapsulations of the dll calls making them easier to work with. Alyce is credited with the encapsulation. Here are the sub programs:
Sub DeleteDC hDC
CallDLL #gdi32, "DeleteDC",hDC As Long, r As Boolean
End Sub
Sub ReleaseDC hWnd, hDC
CallDLL#user32,"ReleaseDC",hWnd As Long,hDC As Long,result As Long
End Sub
Notice that DeleteDC only requires the DC's handle, but ReleaseDC requires both the DC's handle and the window handle that contains the container that the DC refers to.
Now this brings us to the heart of the dissolve program. It is implemented in the "do-it" event handler. The event handler plays double roles, as it both runs the dissolve as well as resets the images for another dissolve run, depending on the state of the command button associated with the "do-it" event.
After every dissolve is complete the text of the command button is changed to "reset" as you will see later, and the state of the program (which is tracked in the "reset" variable) is set to 1.
Later when the program is running, and the "reset" state is 1, and the user presses the command button, causing the code in the event handler to be executed, the program is designed to replace the "coins" image with the original "leaves" image and set the command button text to "Do It", while setting the "reset" variable to zero (0) again. This is all handled in the first section of code in the "do-it" event handler as shown below:
[doit]
if reset = 1 then
'Change window title and fade
call SetWindowText hWnd, "Dissolve Demo Ready"
#main.gb1 "drawbmp leaves 0 0"
#main.doit "Do It!"
reset = 0
Notice that I call a sub program called "SetWindowText" which is another dll call encapsulated in a sub courtesy Alyce. This allows me to alter the main window's titlebar. I use the titlebar as a communication agent between the program and the user. It is a simple and effective tool when you are trying to communicate things such as status or the state of the application. The sub is shown below:
Sub SetWindowText hWnd, txt$
CallDLL #user32, "SetWindowTextA", hWnd As Long, txt$ As Ptr, result As Void
End Sub
The rest of the "do-it" event handler is dedicated to performing the dissolve (or fade) operation. It begins by first setting the program's titlebar to reflect the current status:
else
'Change window title and fade
call SetWindowText hWnd, "Dissolving..."
After which we begin executing a For-Next loop:
for x = 1 to maxpixels
px = pixelsetx(x)
py = pixelsety(x)
'use bltbit to move 4 pixels at once from source in memory to dest
call BitBlt gb1DC, px, py, 2, 2, chDC, px, py
scan
next x
The loop is executed "maxpixels" times, which is the number of pixels we will be moving from the source to the destination, as determined earlier. The actual pixel pairs are randomly distributed within the pair of arrays that hold the x and y coordinates thanks to our randomizing earlier. Now all we have to do is start at the beginning of the arrays and roll through them, passing the x and y coordinate pairs on to our function to move the pixels.
I use the windows api BLTBIT, which is the bulk bit copy routine, to move multiple pixels at one time. This API call is the heart of many graphic functions, including LB's own sprite capabilities. For more information on the call, please checkout http://www.mentalis.org/apilist/BitBlt.shtml. This is yet another API function that Alyce has provided encapsulation for in the form of a sub program. I like to use these functions and subs as they simplify my applications many times. Here is the sub:
Sub BitBlt hDCdest,x,y,w,h,hDCsrc,x2,y2
CallDLL #gdi32, "BitBlt",hDCdest As Long,x As Long,y As Long,w As Long,h As Long,_
hDCsrc As Long,x2 As Long,y2 As Long,_SRCCOPY As Ulong,RESULT As Boolean
End Sub
Notice that we are copying the pixels (as the parameter "_SRCCOPY" would indicate in the sub. There are many other options available which makes the API call very powerful. We are specifying the source as the DC we created in memory and the destination as the DC for the graphic box. We are copying a 2 square pixel area which corresponds to my earlier preparation work.
Once the For-Next loop has completed I change the program's titlebar once more:
call SetWindowText hWnd, "Dissolve Complete"
#main.doit "Reset"
reset = 1
end if
wait
…and then wait for the next user interaction.
That is really all there is to it. The compatibleDC can be expanded in many ways. With windows GDI commands it should be possible to create the DC and then draw in it without ever showing the drawing. Then copy the drawing to a visible graphicbox. This could allow Sprites to be created on the fly, or used for simple frame based animation. The possibilities are as endless as the imagination.
The code for the complete program follows. Remember the entire project is available in archive form in the newsletter download.
The Code -
'fade3.bas - by Brad Moore, copyright 2004
'with gracious thanks to Alyce Watson for many of the API calls
' and help with the Windows GDI calls.
'This program is open source. If you fork it, please rename your
' creation. - Thanks
'-------------------------------------------------------------------------
'I am going to prep the project by building a fade array of points to fade
'this will make the actual fade run faster
WindowWidth = 390 : WindowHeight = 85
UpperLeftX = INT((DisplayWidth-WindowWidth)/2)
UpperLeftY = INT((DisplayHeight-WindowHeight)/2)
statictext #init.st1, "Preparing Data - please wait...", 15, 10, 380, 35
Open "Intializing" for Window as #init
#init "trapclose [cantquit]"
#init "font arial 18 bold"
'load the Original BMP
loadbmp "leaves", "leaves.bmp"
'load the Source BMP
loadbmp "coins", "coins.bmp"
'get the handle of the source BMP
hPic=hBmp("coins")
'drawing size - make sure this is divisible by 2
maxx = 180
maxy = 180
'build an array of x and y pixels to swap
'I will move groups of four pixels so the fade works faster.
xbound = (maxx/2)
ybound = (maxy/2)
maxpixels = xbound * ybound
dim pixelsetx(maxpixels)
dim pixelsety(maxpixels)
for x = 1 to xbound
for y = 1 to ybound
pixelsetx(((x-1)*xbound)+y) = ((x-1)*2)
pixelsety(((x-1)*xbound)+y) = ((y-1)*2)
next y
next x
'now a few iterations to mix stuff up
for q = 1 to 15000
src = int(rnd(0)*(maxpixels))+1
dest = src
while dest = src
dest = int(rnd(0)*(maxpixels))+1
wend
'now swap the elements
tempx = pixelsetx(dest)
tempy = pixelsety(dest)
pixelsetx(dest) = pixelsetx(src)
pixelsety(dest) = pixelsety(src)
pixelsetx(src) = tempx
pixelsety(src) = tempy
next q
[cantquit]
scan
close #init
reset = 0 'tells program whether to reset image first before dissolve
[WindowSetup]
NOMAINWIN
WindowWidth = 308 : WindowHeight = 240
UpperLeftX = INT((DisplayWidth-WindowWidth)/2)
UpperLeftY = INT((DisplayHeight-WindowHeight)/2)
[ControlSetup]
graphicbox #main.gb1, 10, 10, 180, 180
button #main.doit, "Do It!",[doit],UL, 195, 150, 95, 40
Open "Window Title" for Window as #main
#main "trapclose [quit]"
#main "font ms_sans_serif 10 bold"
#main.gb1 "down; fill White; flush"
#main.gb1 "drawbmp leaves 0 0"
#main.gb1 "flush"
'get the window handle
hWnd = hwnd(#main)
'get handle to the graphicbox
hgb1 = hwnd(#main.gb1)
'get each DC for the graphicbox
gb1DC = GetDC(hgb1)
'create a compatible DC (in memory)
chDC = CreateCompatibleDC(gb1DC)
'select the Object (the picture in memory) into the compatible DC
result = SelectObject(chDC, hPic)
[loop]
Wait
[quit]
call ReleaseDC hWnd, gb1DC
call DeleteDC chDC
unloadbmp "leaves"
unloadbmp "coins"
close #main
END
[doit]
if reset = 1 then
'Change window title and fade
call SetWindowText hWnd, "Dissolve Demo Ready"
#main.gb1 "drawbmp leaves 0 0"
#main.doit "Do It!"
reset = 0
else
'Change window title and fade
call SetWindowText hWnd, "Dissolving..."
for x = 1 to maxpixels
px = pixelsetx(x)
py = pixelsety(x)
'use bltbit to move 4 pixels at once from source in memory to dest
call BitBlt gb1DC, px, py, 2, 2, chDC, px, py
scan
next x
call SetWindowText hWnd, "Dissolve Complete"
#main.doit "Reset"
reset = 1
end if
wait
Function GetDC(hWnd)
CallDLL #user32, "GetDC",hWnd As Long,GetDC As Long
End Function
Function CreateCompatibleDC(hDC)
CallDLL #gdi32,"CreateCompatibleDC", hDC As Long, CreateCompatibleDC As Long
End Function
Sub DeleteDC hDC
CallDLL #gdi32, "DeleteDC",hDC As Long, r As Boolean
End Sub
Function SelectObject(hDC,hObject)
CallDLL #gdi32,"SelectObject",hDC As Long,hObject As Long,SelectObject As Long
'returns previously selected object
End Function
Sub BitBlt hDCdest,x,y,w,h,hDCsrc,x2,y2
CallDLL #gdi32, "BitBlt",hDCdest As Long,x As Long,y As Long,w As Long,h As Long,_
hDCsrc As Long,x2 As Long,y2 As Long,_SRCCOPY As Ulong,RESULT As Boolean
End Sub
Sub ReleaseDC hWnd, hDC
CallDLL#user32,"ReleaseDC",hWnd As Long,hDC As Long,result As Long
End Sub
Sub SetWindowText hWnd, txt$
CallDLL #user32, "SetWindowTextA", hWnd As Long, txt$ As Ptr, result As Void
End Sub